{- This file is part of irc-fun-bot.
 -
 - Written in 2015, 2016 by fr33domlover <fr33domlover@riseup.net>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

module Network.IRC.Fun.Bot.Internal.State
    ( askConfig
    , askConfigS
    , askBehavior
    , askBehaviorS
    , askEnv
    , askEnvS
    , askConnection
    , askTimeGetter
    , askMinuteGetter
    , getMinutes
    , getState
    , getStateS
    , putState
    , modifyState
    , getHistory
    , stateToInfo
    , getChanInfo
    , getChans
    , putChans
    , modifyChans
    , addCurrChan
    , removeCurrChan
    , clearCurrChans
    , channelSelected
    , botMemberOf
    , defRespEnabled
    , setDefResp
    )
where

import Control.Monad (liftM)
import Control.Monad.IO.Class (liftIO)
import Data.HashMap.Lazy (HashMap)
import Data.Int (Int64)
import Data.Maybe (fromMaybe, isJust)
import Data.Sequence (Seq)
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Network.IRC.Fun.Bot.Internal.Monad
import Network.IRC.Fun.Bot.Internal.Types
import Network.IRC.Fun.Client.IO (Connection)
import Network.IRC.Fun.Types (Channel)

import qualified Data.HashMap.Lazy as M
import qualified Data.HashSet as S

-- | Fetch the bot configuration.
askConfig :: Session e s Config
askConfig = asks beConfig

-- | Retrieve a function of the bot configuration.
askConfigS :: (Config -> a) -> Session e s a
askConfigS f = asks $ f . beConfig

-- | Fetch the bot behavior definition.
askBehavior :: Session e s (Behavior e s)
askBehavior = asks beBehavior

-- | Retrieve a function of the bot behavior definition.
askBehaviorS :: (Behavior e s -> a) -> Session e s a
askBehaviorS f = asks $ f . beBehavior

-- | Fetch the bot environment, i.e. read-only state.
askEnv :: Session e s e
askEnv = asks beCustom

-- | Retrieve a function of the bot environment.
askEnvS :: (e -> a) -> Session e s a
askEnvS f = asks $ f . beCustom

-- | Fetch the bot session socket handle.
askConnection :: Session e s Connection
askConnection = asks beConn

-- | Fetch the time getter. The actual time data is cached and updated at most
-- once per second depending on need. You can safely use it at any frequency
-- withou overloading IO and time formatting.
--
-- The second item is a formatted time string in the form
-- @2015-09-01 18:10:00@, and is always expressed in UTC.
askTimeGetter :: Session e s (IO (UTCTime, Text))
askTimeGetter = asks beGetTime

-- | Fetch the minute getter. The actual data is cached and updated at most
-- once per minute depending on need. You can safely use it at any frequency
-- withou overloading IO and time formatting.
askMinuteGetter :: Session e s (IO Int64)
askMinuteGetter = asks beGetMinute

-- | Get the number of minutes since the epoch using an auto updating counter.
getMinutes :: Session e s Int64
getMinutes = do
    getMin <- askMinuteGetter
    liftIO getMin

-- | Fetch the current value of the state within the session.
getState :: Session e s s
getState = gets bsPublic

-- | Get a specific component of the state, using a projection function
-- supplied.
getStateS :: (s -> a) -> Session e s a
getStateS f = gets $ f . bsPublic

-- | Set the state within the session.
putState :: s -> Session e s ()
putState st = modify $ \ old -> old { bsPublic = st }

-- | Update the state to the result of applying a function to the current
-- state.
modifyState :: (s -> s) -> Session e s ()
modifyState f =
    modify $ \ old@(BotState { bsPublic = st }) -> old { bsPublic = f st }

-- | Get a mapping between channel names and sequences of their last messages.
getHistory :: Session e s (HashMap Channel (Seq HistoryLine))
getHistory = gets bsHistory

-- Create channel info from its state
stateToInfo :: ChanState -> ChanInfo
stateToInfo (ChanState track count mlogger hls dr) =
    ChanInfo track count (isJust mlogger) hls dr

-- | Get channel state information, in the form of a mapping from channel names
-- to their data.
getChanInfo :: Session e s (HashMap Channel ChanInfo)
getChanInfo = fmap (M.map stateToInfo) getChans

-- Get the channel state map.
getChans :: Session e s (HashMap Channel ChanState)
getChans = gets bsChannels

-- Set a new value for the channel state map.
putChans :: HashMap Channel ChanState -> Session e s ()
putChans chans = modify $ \ s -> s { bsChannels = chans }

-- Update the channel state map value with the result of applying a function.
modifyChans :: (HashMap Channel ChanState -> HashMap Channel ChanState)
            -> Session e s ()
modifyChans f = modify $ \ s -> s { bsChannels = f $ bsChannels s }

-- Add a channel to the list of currently really-joined channels.
addCurrChan :: Channel -> Session e s ()
addCurrChan chan =
    let f = S.insert chan
    in  modify $ \ s -> s { bsCurrChans = f $ bsCurrChans s }

-- Remove a channel from the list of currently really-joined channels.
removeCurrChan :: Channel -> Session e s ()
removeCurrChan chan =
    let f = S.delete chan
    in  modify $ \ s -> s { bsCurrChans = f $ bsCurrChans s }

-- Remove all channels from the list of currently really-joined channels.
clearCurrChans :: Session e s ()
clearCurrChans = modify $ \ s -> s { bsCurrChans = S.empty }

-- | Check whether a channel is listed in persistent state to be joined.
channelSelected :: Channel -> Session e s Bool
channelSelected chan = liftM (chan `S.member`) $ gets bsSelChans

-- | Check whether, as far as the bot knows, if it currently a member of the
-- given channel. Currently kicks, bans, etc. are fully tracked, therefore this
-- information isn't 100% accurate, but if you aren't planning to ban your bot
-- you can probably rely on it.
botMemberOf :: Channel -> Session e s Bool
botMemberOf chan = liftM (chan `S.member`) $ gets bsCurrChans

-- | Check whether a given channel has nonexistent command response enabled.
defRespEnabled :: Channel -> Session e s Bool
defRespEnabled chan = do
    chans <- getChans
    return $ fromMaybe False $ fmap csDefResponse $ M.lookup chan chans

-- | Enable or disable nonexistent command response for a channel. Return
-- whether the new state differs from the previous one. If no such channel
-- exists in the bot state, nothing changes.
setDefResp :: Channel -> Bool -> Session e s Bool
setDefResp chan yes = do
    chans <- getChans
    case M.lookup chan chans of
        Just cstate ->
            if csDefResponse cstate /= yes
                then do
                    let cstate' = cstate { csDefResponse = yes }
                        chans' = M.insert chan cstate' chans
                    putChans chans'
                    return True
                else return False
        Nothing -> return False
